package com.ideaaedi.springcloud.jd.user.config;

import com.alibaba.fastjson2.JSON;
import com.ideaaedi.springcloud.jd.commonds.entity.BaseCodeMsgEnum;
import com.ideaaedi.springcloud.jd.commonds.entity.Result;
import com.ideaaedi.springcloud.jd.user.exception.JdOAuth2Exception;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.common.DefaultThrowableAnalyzer;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint;
import org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint;
import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.security.web.util.ThrowableAnalyzer;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;

/**
 * oauth2认证授权服务器endpoint增强器
 *
 * @author <font size = "20" color = "#3CAA3C"><a href="https://gitee.com/JustryDeng">JustryDeng</a></font> <img src="https://gitee.com/JustryDeng/shared-files/raw/master/JustryDeng/avatar.jpg" />
 * @since 2021.0.1.A
 */
@Slf4j
@Aspect
@Component
@SuppressWarnings({"deprecation", "unchecked", "AlibabaClassNamingShouldBeCamel"})
public class OAuth2ServerEndpointEnhancer {
    
    private final ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
    
    /**
     * 增强 {@link AuthorizationEndpoint}， 使得：当AuthorizationEndpoint里面的endpoint发生异常时，返回自定义的json数据
     */
    @Around("execution(public * org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint"
            + ".handle*Exception(..))")
    public ModelAndView handleAuthorizationEndpoint(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Object[] args = proceedingJoinPoint.getArgs();
        if (args != null && args[0] instanceof JdOAuth2Exception) {
            ServletWebRequest servletWebRequest = (ServletWebRequest) args[1];
            return handleJdOAuth2Exception((JdOAuth2Exception) args[0], servletWebRequest);
        }
        return (ModelAndView) proceedingJoinPoint.proceed(args);
    }
    
    /**
     * 处理 {@link JdOAuth2Exception}
     */
    @SuppressWarnings("AlibabaLowerCamelCaseVariableNaming")
    public ModelAndView handleJdOAuth2Exception(JdOAuth2Exception e, ServletWebRequest webRequest) {
        String requestUri = null;
        if (webRequest != null) {
            requestUri = webRequest.getRequest().getRequestURI();
        }
        log.info("hit handleAuthorizationEndpoint handleJdOAuth2Exception. requestUri -> {}", requestUri, e);
        Result<?> result = e.getResult();
        ModelAndView modelAndView =
                new ModelAndView(new MappingJackson2JsonView()).addAllObjects(JSON.parseObject(JSON.toJSONString(result)));
        modelAndView.setStatus(HttpStatus.BAD_REQUEST);
        return modelAndView;
    }
    
    /**
     * 增强 {@link TokenEndpoint}， 使得：当TokenEndpoint里面的endpoint发生异常时，返回自定义的json数据 <br/><br/>
     * 提示：oauth2中，如果endpoint返回值类型是ResponseEntity<T>，那么需要自定义返回的数据的话，标准做法是 在配置项目的AuthorizationServerConfigurerAdapter时，通过
     * {@link AuthorizationServerEndpointsConfigurer#exceptionTranslator(WebResponseExceptionTranslator)}
     * 指定异常处理器来处理（本质上其实也是指定ResponseEntity<T>中T类的序列化反序列化实现类来完成的）
     */
    @Around("execution(public * org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.handle*Exception("
            + "..)) || execution(public * org.springframework.security.oauth2.provider.endpoint.TokenEndpoint"
            + ".handleException(..))")
    public ResponseEntity<OAuth2Exception> handleTokenEndpoint(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Object[] args = proceedingJoinPoint.getArgs();
        if (args != null && args[0] instanceof Exception) {
            Exception  exception = (Exception)args[0];
            final Throwable[] chain = throwableAnalyzer.determineCauseChain(exception);
            //noinspection AlibabaLowerCamelCaseVariableNaming
            JdOAuth2Exception jdOAuth2Exception =
                    (JdOAuth2Exception) throwableAnalyzer.getFirstThrowableOfType(JdOAuth2Exception.class, chain);
                HttpHeaders headers = new HttpHeaders();
                headers.set("Cache-Control", "no-store");
                headers.set("Pragma", "no-cache");
                headers.set("Content-Type", "application/json;charset=UTF-8");
            if (jdOAuth2Exception != null) {
                log.error("hit handleAuthorizationEndpoint handleTokenEndpoint. for jdOAuth2Exception.", jdOAuth2Exception);
                return new ResponseEntity<>(jdOAuth2Exception, headers, HttpStatus.OK);
            }
            InvalidGrantException invalidGrantException =
                    (InvalidGrantException) throwableAnalyzer.getFirstThrowableOfType(InvalidGrantException.class, chain);
            if (invalidGrantException != null) {
                log.error("hit handleAuthorizationEndpoint handleTokenEndpoint. for invalidGrantException.", invalidGrantException);
                jdOAuth2Exception = new JdOAuth2Exception(Result.failure(invalidGrantException.getMessage(), BaseCodeMsgEnum.USER_PASSWORD_ERROR), invalidGrantException.getMessage(), invalidGrantException);
                return new ResponseEntity<>(jdOAuth2Exception, headers, HttpStatus.OK);
            }
        }
        return (ResponseEntity<OAuth2Exception>) proceedingJoinPoint.proceed(args);
    }
    
    /**
     * 增强 {@link CheckTokenEndpoint}， 使得：当CheckTokenEndpoint里面的endpoint发生异常时，返回自定义的json数据 <br/><br/>
     * 提示：oauth2中，如果endpoint返回值类型是ResponseEntity<T>，那么需要自定义返回的数据的话，标准做法是 在配置项目的AuthorizationServerConfigurerAdapter时，通过
     * {@link AuthorizationServerEndpointsConfigurer#exceptionTranslator(WebResponseExceptionTranslator)}
     * 指定异常处理器来处理（本质上其实也是指定ResponseEntity<T>中T类的序列化反序列化实现类来完成的）
     */
    @Around("execution(public * org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint.handleException(..))")
    public ResponseEntity<OAuth2Exception> handleCheckTokenEndpoint(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Object[] args = proceedingJoinPoint.getArgs();
        if (args != null && args[0] instanceof Exception) {
            Exception  exception = (Exception)args[0];
            final Throwable[] chain = throwableAnalyzer.determineCauseChain(exception);
            //noinspection AlibabaLowerCamelCaseVariableNaming
            JdOAuth2Exception jdOAuth2Exception =
                    (JdOAuth2Exception) throwableAnalyzer.getFirstThrowableOfType(JdOAuth2Exception.class, chain);
                HttpHeaders headers = new HttpHeaders();
                headers.set("Cache-Control", "no-store");
                headers.set("Pragma", "no-cache");
                headers.set("Content-Type", "application/json;charset=UTF-8");
            if (jdOAuth2Exception != null) {
                log.error("hit handleAuthorizationEndpoint handleCheckTokenEndpoint. for jdOAuth2Exception.", jdOAuth2Exception);
                return new ResponseEntity<>(jdOAuth2Exception, headers, HttpStatus.OK);
            }
            InvalidTokenException invalidTokenException =
                    (InvalidTokenException) throwableAnalyzer.getFirstThrowableOfType(InvalidTokenException.class, chain);
            if (invalidTokenException != null) {
                log.error("hit handleAuthorizationEndpoint handleCheckTokenEndpoint. for invalidTokenException.", invalidTokenException);
                jdOAuth2Exception = new JdOAuth2Exception(Result.failure(invalidTokenException.getMessage(), BaseCodeMsgEnum.TOKEN_INVALID), invalidTokenException.getMessage(), invalidTokenException);
                return new ResponseEntity<>(jdOAuth2Exception, headers, HttpStatus.OK);
            }
        }
        return (ResponseEntity<OAuth2Exception>) proceedingJoinPoint.proceed(args);
    }
}